package cn.openbiotoken.android;

import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.media.ExifInterface;
import android.net.Uri;
import android.util.Log;

import androidx.annotation.NonNull;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import javax.annotation.Nonnull;

import cn.openbiotoken.Landmark;

import static java.lang.String.format;

public class UriBitmapHelper {

    public UriBitmapHelper(@NonNull final Context context, @NonNull final Uri uri) {
        this.context = Objects.requireNonNull(context);
        this.uri = Objects.requireNonNull(uri);
    }

    @NonNull
    private final transient Context context;

    @NonNull
    private final Uri uri;

    private transient byte[] content;
    private transient Bitmap bitmap;

    public static final int FULL_IMAGE_QUALITY = 100;
    private static final int BUFFER_SIZE = 0x00010000;
    private static final int BITMAP_BYTE_COUNT = 100 * 1024;
    private static final int HEIGHT = 800;
    private static final int WIDTH = 480;

    private static final byte[] LOAD_CONTENT_LOCK = new byte[0];
    private static final byte[] EXTRACT_BITMAP_DEGREE_LOCK = new byte[0];
    private static final byte[] LOAD_BITMAP_LOCK = new byte[0];

    public byte[] loadContent() {

        if (null != content) {
            return content;
        }

        synchronized (LOAD_CONTENT_LOCK) {
            try (@NonNull final InputStream in = openStream()) {
                @NonNull final ByteArrayOutputStream out = new ByteArrayOutputStream();
                @NonNull final byte[] buffer = new byte[BUFFER_SIZE];
                int availableByteCount;
                if ((availableByteCount = in.read(buffer)) > 0) {
                    out.write(buffer, 0, availableByteCount);
                }
                content = out.toByteArray();
                Log.d(getClass().getSimpleName(), format("loadContent(): Content of %s has %d bytes.", uri.toString(), content.length));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        return content;
    }

    public void drawRect(@NonNull final Bitmap bitmap, @NonNull final Rect rect) {

        @NonNull final Paint paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(3);

        @NonNull final Canvas canvas = new Canvas(bitmap);
        canvas.drawBitmap(bitmap, 0, 0, paint);
        canvas.drawRoundRect(new RectF(rect), 5, 5, paint);

        Log.d(getClass().getSimpleName(), format("Draw Rect: left = %d, top = %d, right = %d, bottom = %d.", rect.left, rect.top, rect.right, rect.bottom));
    }

    public void drawLandmarks(@NonNull final Bitmap bitmap, @NonNull final List<Landmark> landmarks) {

        @Nonnull final Paint qualifiedPaint = new Paint();
        qualifiedPaint.setColor(Color.GREEN);
        qualifiedPaint.setStyle(Paint.Style.STROKE);
        qualifiedPaint.setStrokeWidth(10);

        @Nonnull final Paint unqualifiedPaint = new Paint();
        unqualifiedPaint.setColor(Color.RED);
        unqualifiedPaint.setStyle(Paint.Style.STROKE);
        unqualifiedPaint.setStrokeWidth(10);

        @Nonnull final Canvas canvas = new Canvas(bitmap);
        canvas.drawBitmap(bitmap, 0, 0, null);

        @Nonnull final float[] qualifiedPoints = new float[landmarks.size()*2];
        @Nonnull final float[] unqualifiedPoints = new float[landmarks.size()*2];
        int qualifiedCount = -1;
        int unqualifiedCount = -1;
        for (int i = 0; i<landmarks.size(); i++) {
            if (landmarks.get(i).isConfident()) {
                qualifiedCount++;
                qualifiedPoints[qualifiedCount*2] = landmarks.get(i).getX();
                qualifiedPoints[qualifiedCount*2+1] = landmarks.get(i).getY();
            } else {
                unqualifiedCount++;
                unqualifiedPoints[unqualifiedCount*2] = landmarks.get(i).getX();
                unqualifiedPoints[unqualifiedCount*2+1] = landmarks.get(i).getY();
            }
        }

        Log.d(getClass().getSimpleName(), format("drawLandmarks(): Qualified Count = %d, Unqualified Count = %d", qualifiedCount+1, unqualifiedCount+1));
        if (qualifiedCount>=0) {
            Log.d(getClass().getSimpleName(), format("drawLandmarks(): Qualified Points = %s", Arrays.toString(Arrays.copyOf(qualifiedPoints, (1+qualifiedCount)*2))));
            canvas.drawPoints(qualifiedPoints, 0, (1+qualifiedCount)*2, qualifiedPaint);
        }

        if (unqualifiedCount>=0) {
            Log.d(getClass().getSimpleName(), format("drawLandmarks(): Unqualified Points = %s", Arrays.toString(Arrays.copyOf(unqualifiedPoints, (1+unqualifiedCount)*2))));
            canvas.drawPoints(unqualifiedPoints, 0, (1+unqualifiedCount)*2, unqualifiedPaint);
        }
    }

    public int extractBitmapDegree() {

        final ContentResolver resolver = Objects.requireNonNull(context.getContentResolver());
        final ExifInterface exif;

        try {
            synchronized (EXTRACT_BITMAP_DEGREE_LOCK) {
                exif = new ExifInterface(Objects.requireNonNull(resolver.openInputStream(uri)));
            }
        } catch (IOException e) {
            Log.w(getClass().getSimpleName(), format("extractBitmapDegree(): Failed to read EXIF from URI: %s.", uri.toString()));
            e.printStackTrace();
            return 0;
        }

        // 获取图片的旋转信息
        final int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
                ExifInterface.ORIENTATION_NORMAL);
        Log.d(getClass().getSimpleName(), format("extractBitmapDegree(): Orientation from EXIF is %d, check with ExifInterface.ORIENTATION_ROTATE_*.", orientation));

        final int degree;
        switch (orientation) {
            case ExifInterface.ORIENTATION_ROTATE_90:
                degree = 90;
                break;
            case ExifInterface.ORIENTATION_ROTATE_180:
                degree = 180;
                break;
            case ExifInterface.ORIENTATION_ROTATE_270:
                degree = 270;
                break;
            default:
                degree = 0;
        }
        Log.d(getClass().getSimpleName(), format("extractBitmapDegree(): Degree per orientation is %d.", degree));

        return degree;
    }

    public Bitmap loadOriginalBitmap() {
        final InputStream in = openStream();
        final Bitmap bitmap = BitmapFactory.decodeStream(in);
        //final Bitmap bitmap = BitmapFactory.decodeStream(in, null, bitmapOptions);
        closeStream(in);
        return bitmap;
    }

    @NonNull
    public Bitmap loadBitmap() {
        if (null != bitmap) {
            return bitmap;
        }

        synchronized (LOAD_BITMAP_LOCK) {
            @NonNull final Bitmap thumbnail = loadThumbnail(BITMAP_BYTE_COUNT);
            if (null == thumbnail) {
                return null;
            }
            bitmap = rotateBitmap(thumbnail, extractBitmapDegree());
        }
        return bitmap;
    }

    @NonNull
    private Bitmap loadThumbnail(final int byteCount) {

        @NonNull final BitmapFactory.Options boundsOptions = new BitmapFactory.Options();
        boundsOptions.inJustDecodeBounds = true;
        //onlyBoundsOptions.inDither = true;//optional
        boundsOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//optional
        decodeBitmap(boundsOptions);

        final int originalWidth = boundsOptions.outWidth;
        final int originalHeight = boundsOptions.outHeight;
        if ((originalWidth == -1) || (originalHeight == -1)) {
            Log.w(getClass().getSimpleName(), format("loadThumbnail(): Failed to get original width or height. width: %d, height: %d.", originalWidth, originalHeight));
            return null;
        }

        //图片分辨率以480x800为标准
        final float hh = HEIGHT;
        final float ww = WIDTH;
        //缩放比。由于是固定比例缩放，只用高或者宽其中一个数据进行计算即可
        int be = 1;//be=1表示不缩放
        if (originalWidth > originalHeight && originalWidth > ww) {//如果宽度大的话根据宽度固定大小缩放
            be = (int) (originalWidth / ww);
        } else if (originalWidth < originalHeight && originalHeight > hh) {//如果高度高的话根据宽度固定大小缩放
            be = (int) (originalHeight / hh);
        }

        //比例压缩
        @NonNull final BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
        bitmapOptions.inSampleSize = be <= 0 ? 1 : be;//设置缩放比例
        //bitmapOptions.inDither = true;//optional
        bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//optional

        @NonNull final Bitmap bitmap = decodeBitmap(bitmapOptions);

        return compressImage(bitmap, byteCount);
    }

    @NonNull
    private Bitmap decodeBitmap(@NonNull final BitmapFactory.Options bitmapOptions) {
        @NonNull final InputStream in = openStream();
        @NonNull final Bitmap bitmap = BitmapFactory.decodeStream(in, null, bitmapOptions);
        closeStream(in);
        return bitmap;
    }

    public Bitmap compressImage(@NonNull final Bitmap image, final int byteCount) {
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        int quality = FULL_IMAGE_QUALITY;
        do {
            out.reset();
            image.compress(Bitmap.CompressFormat.JPEG, quality, out);
            quality -= 10;
        } while (out.size() > byteCount);

        final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
        closeStream(out);
        final Bitmap bitmap = BitmapFactory.decodeStream(in, null, null);
        closeStream(in);
        return bitmap;
    }

    /**
     * 将图片按照某个角度进行旋转
     *
     * @param bitmap 需要旋转的图片
     * @param degree 旋转角度
     * @return 旋转后的图片
     */
    @NonNull
    private Bitmap rotateBitmap(@NonNull final Bitmap bitmap, final int degree) {

        // 根据旋转角度，生成旋转矩阵
        final Matrix matrix = new Matrix();
        matrix.postRotate(degree);

        Bitmap rotatedBitmap = bitmap;
        try {
            // 将原始图片按照旋转矩阵进行旋转，并得到新的图片
            rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
        } catch (OutOfMemoryError e) {
            Log.w(getClass().getSimpleName(), "rotateBitmapByDegree(): Failed to rotate image because of lack of memory.", e);
        }

        if (bitmap != rotatedBitmap) {
            bitmap.recycle();
        }
        return rotatedBitmap;
    }

    @NonNull
    private InputStream openStream() {
        @NonNull final ContentResolver resolver = context.getContentResolver();
        try {
            return Objects.requireNonNull(resolver.openInputStream(uri));
        } catch (FileNotFoundException e) {
            Log.e(getClass().getSimpleName(), format("Failed to open input stream per URI: %s.", uri), e);
            throw new RuntimeException(e);
        }
    }

    private void closeStream(@NonNull final InputStream in) {
        try {
            in.close();
        } catch (IOException ignored) {
            Log.w(getClass().getSimpleName(), "closeStream(): Failed to close input stream. Ignore this error...", ignored);
        }
    }

    private void closeStream(@NonNull final OutputStream out) {
        try {
            out.close();
        } catch (IOException ignored) {
            Log.w(getClass().getSimpleName(), "closeStream(): Failed to close output stream. Ignore this error...", ignored);
        }
    }

}
